//完成一个简易的shell（可以对命令进行处理的窗口）
//shell就是外壳程序，本质就是自己创建子进程去执行
//shell执行原理就是去处理用户输入的命令行参数，然后对命令进行分割解析，然后调用对应的程序替换，本质是进程替换
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#define LEFT "["
#define RIGHT "]"
#define LABEL "#"
#define DELIM " "
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

int quit=0;
int lastcode=0;
//声明环境变量
extern char**environ;

char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];

//获取shell提示的当前信息
const char* getusername()
{
  return getenv("USER");//在默认环境变量中获取USER
}
const char* gethostname()
{
  return getenv("HOSTNAME");
}

void mygetpwd();
//接收输入的指令：
void interact(char* cline,int size)
{
 mygetpwd();
  printf(LEFT"%s@%s %s"RIGHT,getusername(),gethostname(),pwd);//模拟打印shell提示信息
  char *s =fgets(cline,size,stdin);//行读取，遇到空格不会停止，直到遇到换行符
  assert(s);
  (void)s;//这里s进行void处理是为了提醒编译器这里的s未使用不是我忘记使用，而是出于某种原因不使用的（本质我们只是用来接收fgets返回的值而已，后面无需使用s）
  cline[strlen(cline)-1]='\0';//因为输入的字符串结尾是有我们的换行符的，我们把换行符改为\0代表结束即可
}

//分割字符串：
int splitstring(char commandline[],char *argv[])//argv是一个输出型的指针数组
{
  int i=0;
  argv[i++]=strtok(commandline,DELIM);//strtok函数有记忆功能，可以按照指定的字符分割字符串，每次分割出来一部分字符串并且返回它们的首地址
  //先把程序名字分割出来，接下来分割出来其他部分，strtok函数有记忆功能下次传NULL即可
  while(argv[i++]=strtok(NULL,DELIM));//这里是故意写的赋值，因为最后一次赋值是NULL，返回空地址，此时循环结束即可
  return i-1;//返回有效字符地址的个数
}

void mygetpwd()
{
  getcwd(pwd,sizeof(pwd));//往pwd中输入当前文件路径
}

void test1()
{
  //首先我们要知道的一个小知识：
  printf("%s""%s\n","hello,","world");
  //输出字符串"%s""%s\n"中间的双引号会自动省略，把字符串拼接在一起：“%s%s”
}

//正常命令的执行：
void NormalExecute(char *argv[])
{

    //4,对于普通命令的执行：
    pid_t id=fork();
    if(id<0)
    {
      perror("fork");
      return;
    }
    else if(id==0)
    {
      //子进程去执行命令：
      //使用进程替换的函数去执行对应的命令的程序  
      execvpe(argv[0],argv,environ);//使用execvpe最方便，第一个参数直接写命令的程序的名字，第二个参数就是命令行参数数组（命令行数组第0个位置也是命令的名字，但是含义与第一个参数不同，第一个参数填名字是为了在文件中寻找对应的程序，这里的名字是为了指定找到路径下的哪个程序的名字），第三个参数就是环境变量
      
      //如果替换失败就返回退出码给父进程：
      exit(EXIT_CODE);//使用设置好的退出码就说明子进程执行失败
    }
    else 
    {
      //父进程就需要负责对子进程进行等待
      int status=0;
      pid_t rid=waitpid(id,&status,0);//阻塞式等待
      if(rid==id)
      {
        //等待成功
        lastcode=WEXITSTATUS(status);//获取等待进程结束后的状态码
        
      }
      else 
      {
        perror("wait fail\n");

      }

    }

  
}
int buildcommand(char*argv[],int argc)
{
 //对于内建命令，也就是自己主进程需要执行不能交给子进程去做的命令，比如说cd，cd应该是切换主进程的目录
    if(argc==2&&strcmp(argv[0],"cd")==0)//命令为cd时，命令行参为cd，路径，NULL，此时有效参数数量为2，并且对比命令名称也就是argv[0]和“cd”如果是cd则进行这里的内建命令的操作
    {
      chdir(argv[1]);
      //改变目录之后还要更新当前目录：
      mygetpwd();
      sprintf(getenv("PWD"),"%s",pwd);
      return 1;
    }
    //对于export命令也是内建命令

    //对于一些命令 的特殊处理也可以放在这里：
    //比如：ls命令需要添加颜色，就要加上"--color"
    if(strcmp(argv[0],"ls")==0)
    {
      argv[argc++]="--color";
      argv[argc]=NULL;

    }
   return 0;
}

int main()
{
  while(!quit)//shell是一直运行的除非我们手动退出
  {
    //1，第一步就是用户输入命令，获取命令行：
    interact(commandline,sizeof(commandline));

    //2,解析输入的命令行例如：ls -a -l 是以空格作为分隔的字符串，我们需要进行分割
    int argc=splitstring(commandline,argv);
   
    //3,对输入的指令进行判断
    if(argc==0)//有效命令个数
    {
      continue;//当命令个数为0的时候就下一轮执行即可
    }
    
    //先判断是否是内建命令：
    int n=buildcommand(argv,argc);


    //注意：对于所有的内建命令就是需要单独进行判断，这是无法避免的：
    
    
    //如果不是内建命令就是要普通执行：
    //4,对于普通命令的执行:
    if(!n)
    {
      NormalExecute(argv);//只需要传入命令行参数即可
    } 


  }




  return 0;
}
